pkg-stats 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  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. import requests # URL checking
  26. import json
  27. import certifi
  28. from urllib3 import HTTPSConnectionPool
  29. from urllib3.exceptions import HTTPError
  30. from multiprocessing import Pool
  31. INFRA_RE = re.compile(r"\$\(eval \$\(([a-z-]*)-package\)\)")
  32. URL_RE = re.compile(r"\s*https?://\S*\s*$")
  33. RM_API_STATUS_ERROR = 1
  34. RM_API_STATUS_FOUND_BY_DISTRO = 2
  35. RM_API_STATUS_FOUND_BY_PATTERN = 3
  36. RM_API_STATUS_NOT_FOUND = 4
  37. class Package:
  38. all_licenses = list()
  39. all_license_files = list()
  40. all_versions = dict()
  41. def __init__(self, name, path):
  42. self.name = name
  43. self.path = path
  44. self.infras = None
  45. self.has_license = False
  46. self.has_license_files = False
  47. self.has_hash = False
  48. self.patch_count = 0
  49. self.warnings = 0
  50. self.current_version = None
  51. self.url = None
  52. self.url_status = None
  53. self.url_worker = None
  54. self.latest_version = (RM_API_STATUS_ERROR, None, None)
  55. def pkgvar(self):
  56. return self.name.upper().replace("-", "_")
  57. def set_url(self):
  58. """
  59. Fills in the .url field
  60. """
  61. self.url_status = "No Config.in"
  62. for filename in os.listdir(os.path.dirname(self.path)):
  63. if fnmatch.fnmatch(filename, 'Config.*'):
  64. fp = open(os.path.join(os.path.dirname(self.path), filename), "r")
  65. for config_line in fp:
  66. if URL_RE.match(config_line):
  67. self.url = config_line.strip()
  68. self.url_status = "Found"
  69. fp.close()
  70. return
  71. self.url_status = "Missing"
  72. fp.close()
  73. def set_infra(self):
  74. """
  75. Fills in the .infras field
  76. """
  77. self.infras = list()
  78. with open(self.path, 'r') as f:
  79. lines = f.readlines()
  80. for l in lines:
  81. match = INFRA_RE.match(l)
  82. if not match:
  83. continue
  84. infra = match.group(1)
  85. if infra.startswith("host-"):
  86. self.infras.append(("host", infra[5:]))
  87. else:
  88. self.infras.append(("target", infra))
  89. def set_license(self):
  90. """
  91. Fills in the .has_license and .has_license_files fields
  92. """
  93. var = self.pkgvar()
  94. if var in self.all_licenses:
  95. self.has_license = True
  96. if var in self.all_license_files:
  97. self.has_license_files = True
  98. def set_hash_info(self):
  99. """
  100. Fills in the .has_hash field
  101. """
  102. hashpath = self.path.replace(".mk", ".hash")
  103. self.has_hash = os.path.exists(hashpath)
  104. def set_patch_count(self):
  105. """
  106. Fills in the .patch_count field
  107. """
  108. self.patch_count = 0
  109. pkgdir = os.path.dirname(self.path)
  110. for subdir, _, _ in os.walk(pkgdir):
  111. self.patch_count += len(fnmatch.filter(os.listdir(subdir), '*.patch'))
  112. def set_current_version(self):
  113. """
  114. Fills in the .current_version field
  115. """
  116. var = self.pkgvar()
  117. if var in self.all_versions:
  118. self.current_version = self.all_versions[var]
  119. def set_check_package_warnings(self):
  120. """
  121. Fills in the .warnings field
  122. """
  123. cmd = ["./utils/check-package"]
  124. pkgdir = os.path.dirname(self.path)
  125. for root, dirs, files in os.walk(pkgdir):
  126. for f in files:
  127. if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host":
  128. cmd.append(os.path.join(root, f))
  129. o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
  130. lines = o.splitlines()
  131. for line in lines:
  132. m = re.match("^([0-9]*) warnings generated", line)
  133. if m:
  134. self.warnings = int(m.group(1))
  135. return
  136. def __eq__(self, other):
  137. return self.path == other.path
  138. def __lt__(self, other):
  139. return self.path < other.path
  140. def __str__(self):
  141. return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \
  142. (self.name, self.path, self.has_license, self.has_license_files, self.has_hash, self.patch_count)
  143. def get_pkglist(npackages, package_list):
  144. """
  145. Builds the list of Buildroot packages, returning a list of Package
  146. objects. Only the .name and .path fields of the Package object are
  147. initialized.
  148. npackages: limit to N packages
  149. package_list: limit to those packages in this list
  150. """
  151. WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"]
  152. WALK_EXCLUDES = ["boot/common.mk",
  153. "linux/linux-ext-.*.mk",
  154. "package/freescale-imx/freescale-imx.mk",
  155. "package/gcc/gcc.mk",
  156. "package/gstreamer/gstreamer.mk",
  157. "package/gstreamer1/gstreamer1.mk",
  158. "package/gtk2-themes/gtk2-themes.mk",
  159. "package/matchbox/matchbox.mk",
  160. "package/opengl/opengl.mk",
  161. "package/qt5/qt5.mk",
  162. "package/x11r7/x11r7.mk",
  163. "package/doc-asciidoc.mk",
  164. "package/pkg-.*.mk",
  165. "package/nvidia-tegra23/nvidia-tegra23.mk",
  166. "toolchain/toolchain-external/pkg-toolchain-external.mk",
  167. "toolchain/toolchain-external/toolchain-external.mk",
  168. "toolchain/toolchain.mk",
  169. "toolchain/helpers.mk",
  170. "toolchain/toolchain-wrapper.mk"]
  171. packages = list()
  172. count = 0
  173. for root, dirs, files in os.walk("."):
  174. rootdir = root.split("/")
  175. if len(rootdir) < 2:
  176. continue
  177. if rootdir[1] not in WALK_USEFUL_SUBDIRS:
  178. continue
  179. for f in files:
  180. if not f.endswith(".mk"):
  181. continue
  182. # Strip ending ".mk"
  183. pkgname = f[:-3]
  184. if package_list and pkgname not in package_list:
  185. continue
  186. pkgpath = os.path.join(root, f)
  187. skip = False
  188. for exclude in WALK_EXCLUDES:
  189. # pkgpath[2:] strips the initial './'
  190. if re.match(exclude, pkgpath[2:]):
  191. skip = True
  192. continue
  193. if skip:
  194. continue
  195. p = Package(pkgname, pkgpath)
  196. packages.append(p)
  197. count += 1
  198. if npackages and count == npackages:
  199. return packages
  200. return packages
  201. def package_init_make_info():
  202. # Licenses
  203. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  204. "-s", "printvars", "VARS=%_LICENSE"])
  205. for l in o.splitlines():
  206. # Get variable name and value
  207. pkgvar, value = l.split("=")
  208. # If present, strip HOST_ from variable name
  209. if pkgvar.startswith("HOST_"):
  210. pkgvar = pkgvar[5:]
  211. # Strip _LICENSE
  212. pkgvar = pkgvar[:-8]
  213. # If value is "unknown", no license details available
  214. if value == "unknown":
  215. continue
  216. Package.all_licenses.append(pkgvar)
  217. # License files
  218. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  219. "-s", "printvars", "VARS=%_LICENSE_FILES"])
  220. for l in o.splitlines():
  221. # Get variable name and value
  222. pkgvar, value = l.split("=")
  223. # If present, strip HOST_ from variable name
  224. if pkgvar.startswith("HOST_"):
  225. pkgvar = pkgvar[5:]
  226. if pkgvar.endswith("_MANIFEST_LICENSE_FILES"):
  227. continue
  228. # Strip _LICENSE_FILES
  229. pkgvar = pkgvar[:-14]
  230. Package.all_license_files.append(pkgvar)
  231. # Version
  232. o = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y",
  233. "-s", "printvars", "VARS=%_VERSION"])
  234. # We process first the host package VERSION, and then the target
  235. # package VERSION. This means that if a package exists in both
  236. # target and host variants, with different version numbers
  237. # (unlikely), we'll report the target version number.
  238. version_list = o.splitlines()
  239. version_list = [x for x in version_list if x.startswith("HOST_")] + \
  240. [x for x in version_list if not x.startswith("HOST_")]
  241. for l in version_list:
  242. # Get variable name and value
  243. pkgvar, value = l.split("=")
  244. # If present, strip HOST_ from variable name
  245. if pkgvar.startswith("HOST_"):
  246. pkgvar = pkgvar[5:]
  247. if pkgvar.endswith("_DL_VERSION"):
  248. continue
  249. # Strip _VERSION
  250. pkgvar = pkgvar[:-8]
  251. Package.all_versions[pkgvar] = value
  252. def check_url_status_worker(url, url_status):
  253. if url_status != "Missing" and url_status != "No Config.in":
  254. try:
  255. url_status_code = requests.head(url, timeout=30).status_code
  256. if url_status_code >= 400:
  257. return "Invalid(%s)" % str(url_status_code)
  258. except requests.exceptions.RequestException:
  259. return "Invalid(Err)"
  260. return "Ok"
  261. return url_status
  262. def check_package_urls(packages):
  263. Package.pool = Pool(processes=64)
  264. for pkg in packages:
  265. pkg.url_worker = pkg.pool.apply_async(check_url_status_worker, (pkg.url, pkg.url_status))
  266. for pkg in packages:
  267. pkg.url_status = pkg.url_worker.get(timeout=3600)
  268. def release_monitoring_get_latest_version_by_distro(pool, name):
  269. try:
  270. req = pool.request('GET', "/api/project/Buildroot/%s" % name)
  271. except HTTPError:
  272. return (RM_API_STATUS_ERROR, None, None)
  273. if req.status != 200:
  274. return (RM_API_STATUS_NOT_FOUND, None, None)
  275. data = json.loads(req.data)
  276. if 'version' in data:
  277. return (RM_API_STATUS_FOUND_BY_DISTRO, data['version'], data['id'])
  278. else:
  279. return (RM_API_STATUS_FOUND_BY_DISTRO, None, data['id'])
  280. def release_monitoring_get_latest_version_by_guess(pool, name):
  281. try:
  282. req = pool.request('GET', "/api/projects/?pattern=%s" % name)
  283. except HTTPError:
  284. return (RM_API_STATUS_ERROR, None, None)
  285. if req.status != 200:
  286. return (RM_API_STATUS_NOT_FOUND, None, None)
  287. data = json.loads(req.data)
  288. projects = data['projects']
  289. projects.sort(key=lambda x: x['id'])
  290. for p in projects:
  291. if p['name'] == name and 'version' in p:
  292. return (RM_API_STATUS_FOUND_BY_PATTERN, p['version'], p['id'])
  293. return (RM_API_STATUS_NOT_FOUND, None, None)
  294. def check_package_latest_version(packages):
  295. """
  296. Fills in the .latest_version field of all Package objects
  297. This field has a special format:
  298. (status, version, id)
  299. with:
  300. - status: one of RM_API_STATUS_ERROR,
  301. RM_API_STATUS_FOUND_BY_DISTRO, RM_API_STATUS_FOUND_BY_PATTERN,
  302. RM_API_STATUS_NOT_FOUND
  303. - version: string containing the latest version known by
  304. release-monitoring.org for this package
  305. - id: string containing the id of the project corresponding to this
  306. package, as known by release-monitoring.org
  307. """
  308. pool = HTTPSConnectionPool('release-monitoring.org', port=443,
  309. cert_reqs='CERT_REQUIRED', ca_certs=certifi.where(),
  310. timeout=30)
  311. count = 0
  312. for pkg in packages:
  313. v = release_monitoring_get_latest_version_by_distro(pool, pkg.name)
  314. if v[0] == RM_API_STATUS_NOT_FOUND:
  315. v = release_monitoring_get_latest_version_by_guess(pool, pkg.name)
  316. pkg.latest_version = v
  317. print("[%d/%d] Package %s" % (count, len(packages), pkg.name))
  318. count += 1
  319. def calculate_stats(packages):
  320. stats = defaultdict(int)
  321. for pkg in packages:
  322. # If packages have multiple infra, take the first one. For the
  323. # vast majority of packages, the target and host infra are the
  324. # same. There are very few packages that use a different infra
  325. # for the host and target variants.
  326. if len(pkg.infras) > 0:
  327. infra = pkg.infras[0][1]
  328. stats["infra-%s" % infra] += 1
  329. else:
  330. stats["infra-unknown"] += 1
  331. if pkg.has_license:
  332. stats["license"] += 1
  333. else:
  334. stats["no-license"] += 1
  335. if pkg.has_license_files:
  336. stats["license-files"] += 1
  337. else:
  338. stats["no-license-files"] += 1
  339. if pkg.has_hash:
  340. stats["hash"] += 1
  341. else:
  342. stats["no-hash"] += 1
  343. if pkg.latest_version[0] == RM_API_STATUS_FOUND_BY_DISTRO:
  344. stats["rmo-mapping"] += 1
  345. else:
  346. stats["rmo-no-mapping"] += 1
  347. if not pkg.latest_version[1]:
  348. stats["version-unknown"] += 1
  349. elif pkg.latest_version[1] == pkg.current_version:
  350. stats["version-uptodate"] += 1
  351. else:
  352. stats["version-not-uptodate"] += 1
  353. stats["patches"] += pkg.patch_count
  354. return stats
  355. html_header = """
  356. <head>
  357. <script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script>
  358. <style type=\"text/css\">
  359. table {
  360. width: 100%;
  361. }
  362. td {
  363. border: 1px solid black;
  364. }
  365. td.centered {
  366. text-align: center;
  367. }
  368. td.wrong {
  369. background: #ff9a69;
  370. }
  371. td.correct {
  372. background: #d2ffc4;
  373. }
  374. td.nopatches {
  375. background: #d2ffc4;
  376. }
  377. td.somepatches {
  378. background: #ffd870;
  379. }
  380. td.lotsofpatches {
  381. background: #ff9a69;
  382. }
  383. td.good_url {
  384. background: #d2ffc4;
  385. }
  386. td.missing_url {
  387. background: #ffd870;
  388. }
  389. td.invalid_url {
  390. background: #ff9a69;
  391. }
  392. td.version-good {
  393. background: #d2ffc4;
  394. }
  395. td.version-needs-update {
  396. background: #ff9a69;
  397. }
  398. td.version-unknown {
  399. background: #ffd870;
  400. }
  401. td.version-error {
  402. background: #ccc;
  403. }
  404. </style>
  405. <title>Statistics of Buildroot packages</title>
  406. </head>
  407. <a href=\"#results\">Results</a><br/>
  408. <p id=\"sortable_hint\"></p>
  409. """
  410. html_footer = """
  411. </body>
  412. <script>
  413. if (typeof sorttable === \"object\") {
  414. document.getElementById(\"sortable_hint\").innerHTML =
  415. \"hint: the table can be sorted by clicking the column headers\"
  416. }
  417. </script>
  418. </html>
  419. """
  420. def infra_str(infra_list):
  421. if not infra_list:
  422. return "Unknown"
  423. elif len(infra_list) == 1:
  424. return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0])
  425. elif infra_list[0][1] == infra_list[1][1]:
  426. return "<b>%s</b><br/>%s + %s" % \
  427. (infra_list[0][1], infra_list[0][0], infra_list[1][0])
  428. else:
  429. return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \
  430. (infra_list[0][1], infra_list[0][0],
  431. infra_list[1][1], infra_list[1][0])
  432. def boolean_str(b):
  433. if b:
  434. return "Yes"
  435. else:
  436. return "No"
  437. def dump_html_pkg(f, pkg):
  438. f.write(" <tr>\n")
  439. f.write(" <td>%s</td>\n" % pkg.path[2:])
  440. # Patch count
  441. td_class = ["centered"]
  442. if pkg.patch_count == 0:
  443. td_class.append("nopatches")
  444. elif pkg.patch_count < 5:
  445. td_class.append("somepatches")
  446. else:
  447. td_class.append("lotsofpatches")
  448. f.write(" <td class=\"%s\">%s</td>\n" %
  449. (" ".join(td_class), str(pkg.patch_count)))
  450. # Infrastructure
  451. infra = infra_str(pkg.infras)
  452. td_class = ["centered"]
  453. if infra == "Unknown":
  454. td_class.append("wrong")
  455. else:
  456. td_class.append("correct")
  457. f.write(" <td class=\"%s\">%s</td>\n" %
  458. (" ".join(td_class), infra_str(pkg.infras)))
  459. # License
  460. td_class = ["centered"]
  461. if pkg.has_license:
  462. td_class.append("correct")
  463. else:
  464. td_class.append("wrong")
  465. f.write(" <td class=\"%s\">%s</td>\n" %
  466. (" ".join(td_class), boolean_str(pkg.has_license)))
  467. # License files
  468. td_class = ["centered"]
  469. if pkg.has_license_files:
  470. td_class.append("correct")
  471. else:
  472. td_class.append("wrong")
  473. f.write(" <td class=\"%s\">%s</td>\n" %
  474. (" ".join(td_class), boolean_str(pkg.has_license_files)))
  475. # Hash
  476. td_class = ["centered"]
  477. if pkg.has_hash:
  478. td_class.append("correct")
  479. else:
  480. td_class.append("wrong")
  481. f.write(" <td class=\"%s\">%s</td>\n" %
  482. (" ".join(td_class), boolean_str(pkg.has_hash)))
  483. # Current version
  484. if len(pkg.current_version) > 20:
  485. current_version = pkg.current_version[:20] + "..."
  486. else:
  487. current_version = pkg.current_version
  488. f.write(" <td class=\"centered\">%s</td>\n" % current_version)
  489. # Latest version
  490. if pkg.latest_version[0] == RM_API_STATUS_ERROR:
  491. td_class.append("version-error")
  492. if pkg.latest_version[1] is None:
  493. td_class.append("version-unknown")
  494. elif pkg.latest_version[1] != pkg.current_version:
  495. td_class.append("version-needs-update")
  496. else:
  497. td_class.append("version-good")
  498. if pkg.latest_version[0] == RM_API_STATUS_ERROR:
  499. latest_version_text = "<b>Error</b>"
  500. elif pkg.latest_version[0] == RM_API_STATUS_NOT_FOUND:
  501. latest_version_text = "<b>Not found</b>"
  502. else:
  503. if pkg.latest_version[1] is None:
  504. latest_version_text = "<b>Found, but no version</b>"
  505. else:
  506. latest_version_text = "<a href=\"https://release-monitoring.org/project/%s\"><b>%s</b></a>" % \
  507. (pkg.latest_version[2], str(pkg.latest_version[1]))
  508. latest_version_text += "<br/>"
  509. if pkg.latest_version[0] == RM_API_STATUS_FOUND_BY_DISTRO:
  510. latest_version_text += "found by <a href=\"https://release-monitoring.org/distro/Buildroot/\">distro</a>"
  511. else:
  512. latest_version_text += "found by guess"
  513. f.write(" <td class=\"%s\">%s</td>\n" %
  514. (" ".join(td_class), latest_version_text))
  515. # Warnings
  516. td_class = ["centered"]
  517. if pkg.warnings == 0:
  518. td_class.append("correct")
  519. else:
  520. td_class.append("wrong")
  521. f.write(" <td class=\"%s\">%d</td>\n" %
  522. (" ".join(td_class), pkg.warnings))
  523. # URL status
  524. td_class = ["centered"]
  525. url_str = pkg.url_status
  526. if pkg.url_status == "Missing" or pkg.url_status == "No Config.in":
  527. td_class.append("missing_url")
  528. elif pkg.url_status.startswith("Invalid"):
  529. td_class.append("invalid_url")
  530. url_str = "<a href=%s>%s</a>" % (pkg.url, pkg.url_status)
  531. else:
  532. td_class.append("good_url")
  533. url_str = "<a href=%s>Link</a>" % pkg.url
  534. f.write(" <td class=\"%s\">%s</td>\n" %
  535. (" ".join(td_class), url_str))
  536. f.write(" </tr>\n")
  537. def dump_html_all_pkgs(f, packages):
  538. f.write("""
  539. <table class=\"sortable\">
  540. <tr>
  541. <td>Package</td>
  542. <td class=\"centered\">Patch count</td>
  543. <td class=\"centered\">Infrastructure</td>
  544. <td class=\"centered\">License</td>
  545. <td class=\"centered\">License files</td>
  546. <td class=\"centered\">Hash file</td>
  547. <td class=\"centered\">Current version</td>
  548. <td class=\"centered\">Latest version</td>
  549. <td class=\"centered\">Warnings</td>
  550. <td class=\"centered\">Upstream URL</td>
  551. </tr>
  552. """)
  553. for pkg in sorted(packages):
  554. dump_html_pkg(f, pkg)
  555. f.write("</table>")
  556. def dump_html_stats(f, stats):
  557. f.write("<a id=\"results\"></a>\n")
  558. f.write("<table>\n")
  559. infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
  560. for infra in infras:
  561. f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" %
  562. (infra, stats["infra-%s" % infra]))
  563. f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" %
  564. stats["license"])
  565. f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" %
  566. stats["no-license"])
  567. f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" %
  568. stats["license-files"])
  569. f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" %
  570. stats["no-license-files"])
  571. f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" %
  572. stats["hash"])
  573. f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" %
  574. stats["no-hash"])
  575. f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" %
  576. stats["patches"])
  577. f.write("<tr><td>Packages having a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  578. stats["rmo-mapping"])
  579. f.write("<tr><td>Packages lacking a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  580. stats["rmo-no-mapping"])
  581. f.write("<tr><td>Packages that are up-to-date</td><td>%s</td></tr>\n" %
  582. stats["version-uptodate"])
  583. f.write("<tr><td>Packages that are not up-to-date</td><td>%s</td></tr>\n" %
  584. stats["version-not-uptodate"])
  585. f.write("<tr><td>Packages with no known upstream version</td><td>%s</td></tr>\n" %
  586. stats["version-unknown"])
  587. f.write("</table>\n")
  588. def dump_gen_info(f):
  589. # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032
  590. o = subprocess.check_output(["git", "log", "master", "-n", "1", "--pretty=format:%H"])
  591. git_commit = o.splitlines()[0]
  592. f.write("<p><i>Updated on %s, git commit %s</i></p>\n" %
  593. (str(datetime.datetime.utcnow()), git_commit))
  594. def dump_html(packages, stats, output):
  595. with open(output, 'w') as f:
  596. f.write(html_header)
  597. dump_html_all_pkgs(f, packages)
  598. dump_html_stats(f, stats)
  599. dump_gen_info(f)
  600. f.write(html_footer)
  601. def parse_args():
  602. parser = argparse.ArgumentParser()
  603. parser.add_argument('-o', dest='output', action='store', required=True,
  604. help='HTML output file')
  605. parser.add_argument('-n', dest='npackages', type=int, action='store',
  606. help='Number of packages')
  607. parser.add_argument('-p', dest='packages', action='store',
  608. help='List of packages (comma separated)')
  609. return parser.parse_args()
  610. def __main__():
  611. args = parse_args()
  612. if args.npackages and args.packages:
  613. print("ERROR: -n and -p are mutually exclusive")
  614. sys.exit(1)
  615. if args.packages:
  616. package_list = args.packages.split(",")
  617. else:
  618. package_list = None
  619. print("Build package list ...")
  620. packages = get_pkglist(args.npackages, package_list)
  621. print("Getting package make info ...")
  622. package_init_make_info()
  623. print("Getting package details ...")
  624. for pkg in packages:
  625. pkg.set_infra()
  626. pkg.set_license()
  627. pkg.set_hash_info()
  628. pkg.set_patch_count()
  629. pkg.set_check_package_warnings()
  630. pkg.set_current_version()
  631. pkg.set_url()
  632. print("Checking URL status")
  633. check_package_urls(packages)
  634. print("Getting latest versions ...")
  635. check_package_latest_version(packages)
  636. print("Calculate stats")
  637. stats = calculate_stats(packages)
  638. print("Write HTML")
  639. dump_html(packages, stats, args.output)
  640. __main__()